/*
 * Copyright (c) 2012-2013 XXXX XXXXXX, Inc.
 * XXXX Road, XXXX, XXXX State, XXXX Post code, XXXX Country.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of XXXX
 * XXXXXX, Inc. ("Confidential Information").  You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with XXXX.
 * 
 * History
 * Date        Ver Author        Change Description
 * ----------- --- ------------- ----------------------------------------
 * 04 Jun 2012 001 Karl          Create
 * 18 Jan 2013 002 Karl          Add method to verify if test can run on current platform. 
 * 05 Feb 2013 003 Karl          Check test script writer running limitations before
 *                               start a test script.
 * 28 Jun 2013 004 Karl          Involve retry logic for test script running.
 * 22 Jul 2013 005 Karl          Experiment with stages. 
 * 07 Aug 2013 006 Karl          Refactor code, move error handling here.
 */

package com.selenium.test.base;

import static com.selenium.test.util.SeleniumUtil.SHOT_TYPE_ERR_STR;
import static com.selenium.test.util.SeleniumUtil.SHOT_TYPE_FAIL_STR;

import java.util.ArrayList;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriverException;

import com.selenium.test.util.CustomTestRunner;
import com.selenium.test.util.ISeleniumUtil;
import com.selenium.test.util.Retry;
import com.selenium.test.util.RunningLimitation;
import com.selenium.test.util.RunningLimitation.RequestedRunningLimitation;
import com.selenium.test.util.RunningLimitation.ViolateRunningLimitationException;
import com.selenium.test.util.SeleniumConfigHolder;
import com.selenium.test.util.SeleniumTestDataHolder;
import com.selenium.test.util.SeleniumUtil;

/**
 * Abstract selenium test class, start a new browser before test and 
 * close it after test. <br/><br/>
 * 
 * Currently it uses fire fox browser by default, this can be configured 
 * outside in a configuration file runtest*.xml. 
 * 
 */
@RunWith(CustomTestRunner.class)
public abstract class SeleniumTestAbstract {
	
	protected ISeleniumUtil seleniumUtil;
	
	@Rule
	public TestRule retry = new Retry(getRetryCount());
	
    /** 
     * There are three ways to run a test script(a Junit java test case).
	 * 
	 * <p>1. If one test case is run from eclipse \ run as JUnit, it will 
	      just read browser name property 
	      "selenium.config.system.browser" from runtest.xml. 
	      Other browser property such as : platform, version, 
	      folder name etc. are ignored completely. 
	      
	      Running a single test from eclipse is just for checking 
	      convenience, not useful in actual production environment.
	      No need to care it too much. </p> 
	      
	   <p>2. If run test by ant, and not use Grid, same as above, only 
	      browser name is used. Other properties are ignored. AND
	      the browser name is extracted from string like: 
	      "-browser browserName=chrome -browser...", not from 
	      "selenium.config.system.browser" property.</p> 
	      
	      Test cases will just use local default installed browsers to 
	      test. It makes sense to ignore version, platform parameter
	      since if running from local machine, browser platform and 
	      version is already fixed.    
	         
	   <p>3. If run test by ant, and set enable grid property, ALL browser
	      short name(take care that it is browser SHORT name extracted
	      from string like "-browser browserName=chrome -browser..." in
	      runtest.xml file), browser version, browser platform are used 
	      to match appropriate Selenium Grid Node.</p> 
	      
	 <PRE>
	 * History
	 * Date        Ver Author        Change Description
	 * ----------- --- ------------- ----------------------------------------
	 * 05 Feb 2013 003 Karl          Check test script writer running limitations before
	 *                               start a test script.  
	 </PRE>
	 */
	@Before
	public void setUp() {
		try{
			List<RunningLimitation> requiredRunningLimitations = getRequiredRunningLimitationsForLocalRun(getClass(), SeleniumUtil.getStackMethodName());
			RequestedRunningLimitation requestedRunningLimitation = SeleniumConfigHolder.getRequestedRunningLimitation();
			RunningLimitation actualRunningLimitation = verifyRunningLimitations(requiredRunningLimitations, requestedRunningLimitation);
			seleniumUtil = SeleniumUtil.newInstance(requiredRunningLimitations, requestedRunningLimitation, actualRunningLimitation);
			
			// maximize browser
			try{
				seleniumUtil.windowMaximize();
			} catch (Exception e){ // This causes error in some situation , such as : selenium grid environment, if it failed, just let it go 
				System.out.println("!Warning: current web driver not support maximize() function, maximize() is ignored.");
			}
		} catch (Exception e){
			e.printStackTrace();
			throw new AssertionError("Error happened when trying to launch a test script. Detailed message is : " + e.getMessage());
		}
	}
	
	/**
	 * Quit browser after each test. 
	 * */
	@After
	public void tearDown() {
		if(seleniumUtil != null){
			if(!seleniumUtil.isInUnstableState()){
				try {seleniumUtil.quit(); } catch(Exception e) {  }
			} else {
				System.out.println("!Warning: web driver is now in unstable state, browser will be let open.");
			}
		} else {
			System.out.println("Web driver not initilized normally, it is null, tearDown " +
				"is ignored.");
		}
	}
	
	/**
	 * Verify if running limitations are fulfilled. If yes, return the actual limitation that is applied
	 * on current test script.
	 * 
	 * @return RunningLimitation the actual limitation which is actual used to start the test script.
	 * @param requiredRunningLimitations required running limitation added by test script writer
	 * @param requestedRunningLimitation running limitation added by client
	 * */
	private RunningLimitation verifyRunningLimitations(List<RunningLimitation> requiredRunningLimitations, 
			RequestedRunningLimitation requestedRunningLimitation) {
		RunningLimitation localMachineRunningLimitation = RunningLimitation.getLocalMachineRunningLimitation();
		// Verify arguments
		if(requestedRunningLimitation == null){
			throw new IllegalArgumentException("Requested running limitation argument can't be null.");
		} 
		
		if(requiredRunningLimitations.size() > 0){
			// Check if requiredRunningLimitations includes invalid options
			List<RunningLimitation> limits = 
				verifyRequiredVsRequestedRunningLimitations(requiredRunningLimitations, requestedRunningLimitation);
			return verifyRequiredVsLocalMachineRunningLimitations(requestedRunningLimitation, limits, localMachineRunningLimitation);
		} else {
			return requestedRunningLimitation;
		}
		
	}
	
	/**
	 * Read required running limitations from TestDataSet.xml file. This limitations are written by test script writer. 
	 * Client no need to modify it directly. Test script writer is responsible to determine which platform a test script
	 * can run on.
	 * */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private List<RunningLimitation> getRequiredRunningLimitationsForLocalRun(Class cls, String methodName){
		List<RunningLimitation> rtn = new ArrayList<RunningLimitation>();
		if(SeleniumTestDataHolder.isPropertyExists(cls, methodName, "RequiredRunningLimitationsForLocalRun")){
			rtn = SeleniumTestDataHolder.getClassMethodObjectList(cls, 
				methodName, "RequiredRunningLimitationsForLocalRun", RunningLimitation.class);
		} else { // test script writer did not give any limitation on running script.
			return rtn;
		}
		
		for(RunningLimitation loopLimit : rtn){
			// (1) Test script writer can not force a test script to run on Selenium Grid   
			if(loopLimit.getUseSeleniumGrid() != null && loopLimit.getUseSeleniumGrid()){
				throw new IllegalArgumentException("Can't limit a test script to run only on Selenium Grid Node. Please ask " +
					"test script writer to set 'useSeleniumGrid' option to false to limit test script on local machine.");
			}
		}
		
		return rtn;
	}
	
	/**
	 * Verify required running limitation is valid by requested running limitation. 
	 * 
	 * <p>
	 * We are now assuming that: </p>
	 * <p>
	 * 1. Test script writer can't force script to run on grid, he is not allowed 
	 *    to set required option useSeleniumGrid as true. In another word: the 
	 *    RequiredRunningLimitations configuration in TestDataSet.xml is used ONLY 
	 *    to indicate how a test script can run on locally machine, such as run 
	 *    on which OS platform(WINDOWS, MAC), which browser(Chrome, Firefox). </p>
	 * <p>
	 * 2. useSeleniumGrid option indicated by test script writer has higher 
	 *    privilege than the value requested by client. If test script writer say 
	 *    a script can run only locally, the requested true value for useSeleniumGrid 
	 *    is just ignored directly. </p>   
	 * <p>
	 * 3. In comment of this method, "required" means required running limitation 
	 *    from TestDataSet.xml by test script writer, "requested" means requested 
	 *    running limitation from runtest*.xml by client.</p>
	 * */
	private List<RunningLimitation> verifyRequiredVsRequestedRunningLimitations(List<RunningLimitation> requiredRunningLimitations, 
			RequestedRunningLimitation requestedRunningLimitation){
		
		if(requiredRunningLimitations == null){
			throw new IllegalArgumentException("Required running limitation argument can't be null.");
		}
		
		// If no required running limitation, just return the requested running limitation
		List<RunningLimitation> rtn = new ArrayList<RunningLimitation>();
		if(requiredRunningLimitations.size() == 0){
			rtn.add(new RunningLimitation(requestedRunningLimitation.getUseSeleniumGrid(), 
					requestedRunningLimitation.getBrowserPlatform(), 
					requestedRunningLimitation.getBrowserShortName() != null ? 
						requestedRunningLimitation.getBrowserShortName() : 
						SeleniumUtil.Browsers.convertToBrowserShortName(requestedRunningLimitation.getBrowserName()), 
					requestedRunningLimitation.getBrowserVersion()));
			return rtn;
		}
		
		// make a copy of requiredRunningLimitations, don't change the passed in one
		List<RunningLimitation> copyOfRequired = new ArrayList<RunningLimitation>();
		for(RunningLimitation loopLimit : requiredRunningLimitations){
			copyOfRequired.add(new RunningLimitation(loopLimit.getUseSeleniumGrid(), 
				loopLimit.getBrowserPlatform(), loopLimit.getBrowserShortName(), 
				loopLimit.getBrowserVersion()));
		}
		
		// If requiredRunningLimitation is applied, the only meaningful parameter for 
		// requested running limitation is the browser name. The other two parameters
		// browserPlatform & browserVersion is ignored. 
		RunningLimitation copyRequested = new RequestedRunningLimitation(
				requestedRunningLimitation.getUseSeleniumGrid(), null, 
				requestedRunningLimitation.getBrowserShortName(), null);
		
		for(RunningLimitation loopLimit : copyOfRequired){
			// (1) Handle grid option, if test script writer does not limit set SeleniumGrid as false, give error message
			// (1.a) no grid option is set, it is not allowed
			if(loopLimit.getUseSeleniumGrid() == null){
				// use client requesting value
				System.out.println("!Warning: Test script writer did not set useSeleniumGrid option, system set it as false automatically.");
				loopLimit.setUseSeleniumGrid(Boolean.FALSE.toString());
			// (1.b) run grid, not allowed
			} else if(loopLimit.getUseSeleniumGrid()){
				throw new RuntimeException("Test script writer can't force script to use Selenium Grid, please set useSeleniumGrid as false.");
			}
			
			/* no need to check browserPlatform request
			// (2) Handle browser platform option
			// (2.a) no value set for platform by test script writer, use client requesting value
			if(loopLimit.getBrowserPlatform() == null){
				if(requestedRunningLimitation.getBrowserPlatform() != null){
					loopLimit.setBrowserPlatform(requestedRunningLimitation.getBrowserPlatform());
				}
			// (2.b) specific value set by script writer, if value not equals with client indicated value, ignore current record
			} else {
				if(requestedRunningLimitation.getBrowserPlatform() != null && 
					!loopLimit.getBrowserPlatform().equals(requestedRunningLimitation.getBrowserPlatform())){
					continue;
				}
			}*/
			
			// (3) Handle browser name option
			// (3.a) if test script writer not limit browser, use client requested value
			if(loopLimit.getBrowserName() == null){
				loopLimit.setBrowserShortName(
					SeleniumUtil.Browsers.convertToBrowserShortName(copyRequested.getBrowserName()));
			// (3.b) if test script writer limit a browser, and client also give a running browser, compare if them equals
			} else if(!loopLimit.getBrowserName().equals(copyRequested.getBrowserName())){
				continue;
			}
			
			rtn.add(loopLimit);
		}
		
		if(rtn.size() == 0){
			throw new ViolateRunningLimitationException("Current test script could not run on requested limitation: " + 
				copyRequested + ". Test script writer had limited this script to run " + 
				"only on following limitations: " + RunningLimitation.toString(requiredRunningLimitations));
		}
		return rtn;
	}
	
	/**
	 * Verify requested running limitation is valid by local machine running limitation.
	 * */
	private RunningLimitation verifyRequiredVsLocalMachineRunningLimitations(
			RequestedRunningLimitation requested,
			List<RunningLimitation> limits,
			RunningLimitation localMachineRunningLimitation) {
		
		if(limits.size() == 0){
			throw new IllegalArgumentException("Running limitation list can't be empty.");
		}
		
		List<RunningLimitation> rtn = new ArrayList<RunningLimitation>();
		for(RunningLimitation loopLimit : limits){
			// test script writer can't do any limitation on Selenium Grid
			if(loopLimit.getUseSeleniumGrid()){
				throw new IllegalArgumentException("Invalid argument found when verifying " +
					"RequiredRunningLimitations by LocalMachineRunningLimitation.");
			}
			if(loopLimit.getBrowserPlatform() == null){
				loopLimit.setBrowserPlatform(localMachineRunningLimitation.getBrowserPlatform());
				rtn.add(loopLimit);
			} else if(loopLimit.getBrowserPlatform().equals(localMachineRunningLimitation.getBrowserPlatform())){
				rtn.add(loopLimit);
			}
		}
		
		if(rtn.size() == 0){
			throw new ViolateRunningLimitationException("Current test script can't run on local machine : " + 
				localMachineRunningLimitation + ", test script writer has limited this test script to run " +
						"only on : " + RunningLimitation.toString(limits));
		}
		
		if(rtn.size() > 1){
			System.out.println("More than one required running limit found for this test script, they are : " + 
				rtn + ", the first one is choosen as actual running limit.");
		}
		return rtn.get(0);
	}
	
	/** 
	 * History
	 * Date        Ver Author        Change Description
	 * ----------- --- ------------- ----------------------------------------
	 * 28 Jun 2013 004 Karl          Involve retry logic for test script running.
	 */
	private int getRetryCount(){
		int retryCount = 1;
		try {
			retryCount = SeleniumTestDataHolder.getRetryCount(getClass());
		} catch (Exception e){
			System.out.println("!Warning: reading retry count error for " + 
				getClass() + ". Error msg is: [" + e.getMessage() + 
				"]. Use default value 1 instead. ");
		}
		return retryCount;
	}
	
	/** 
	 * History
	 * Date        Ver Author        Change Description
	 * ----------- --- ------------- ----------------------------------------
	 * 07 Aug 2013 006 Karl          Refactor code, move error handling here.
	 */
	protected final void screenshotAndThrow(String testMethodName, Throwable throwable){
		getScreenShotIfExceptionAndAssertion(testMethodName, throwable);
		wrapAndThrow(throwable);
	}
	
	private void getScreenShotIfExceptionAndAssertion(
		String testMethodName, Throwable throwable){
		if(throwable instanceof Exception){
			seleniumUtil.getScreenShot(getClass(), testMethodName, SHOT_TYPE_ERR_STR);
		} else if(throwable instanceof AssertionError ){
			seleniumUtil.getScreenShot(getClass(), testMethodName, SHOT_TYPE_FAIL_STR);
		}
	}
	
	private void wrapAndThrow(Throwable throwable) {
		if(throwable instanceof RuntimeException){
			throw (RuntimeException)throwable;
		} else if(throwable instanceof AssertionError){
			throw (AssertionError)throwable;
		} else if (throwable instanceof Error){
			throw (Error)throwable;
		}
		ignoreWrapperTrace(throwable);
	}
	
	/**
	 * To remove trace info for the wrapper RuntimeException. Show the source
	 * exception trace is enough for debugging.
	 * */
	private void ignoreWrapperTrace(Throwable throwable) {
		RuntimeException rtn = new WebDriverException(throwable);
		rtn.setStackTrace(throwable.getStackTrace());
		throw rtn;
	}
}
